//@author A0086393U
package rightnow.ui.widgets;

import java.net.URL;

import org.joda.time.DateTime;
import org.joda.time.DateTimeComparator;
import org.joda.time.Days;
import org.joda.time.Weeks;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import rightnow.storage.Task;
import rightnow.ui.MainApp;
import rightnow.util.DateTimeFormatters;
import rightnow.util.UiOptions;
import rightnow.util.UiUtils;
import javafx.fxml.FXML;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Circle;

/**
 * FX-Controller for the SimpleTaskView.fxml display element.
 * Contains UI logic for managing what to display in the Simple Task View.
 * Not heavily logged because this is created a large number of times.
 */
public class SimpleTaskController extends AnchorPane {
	private static final Logger LOG = LoggerFactory.getLogger(MainApp.class);

	private static final String SIMPLE_TASK_VIEW_FXML = "/fxml/SimpleTaskView.fxml";
	private final URL simpleTaskViewFxmlUrl = getClass().getResource(SIMPLE_TASK_VIEW_FXML);
	
	// Strings to format for the easy-to-read dates/times
	private static final String START_TEXT = "Starts %s\n @ %s";
	private static final String STARTED_TEXT = "Started %s\n @ %s";
	private static final String END_TEXT = "Ends %s\n @ %s";
	private static final String ENDED_TEXT = "Ended %s\n @ %s";
	private static final String TODAY = "today";
	private static final String YESTERDAY = "yesterday";
	private static final String TOMORROW = "tomorrow";
	private static final String THISWEEK_EARLIER = "this past ";
	private static final String THISWEEK_LATER = "this coming ";
	private static final String SOME_DATE = "on %s";
	private static enum Status {
		START, STARTED, END, ENDED
	};
	
	private boolean isTaskOverdue = false;	// will be set during time check
	
	/* These are components in this section of the UI, and they will be
	 * injected into by FXML automatically.
	 * 
	 * DO NOT CREATE NEW INSTANCES OF THESE! (unless you know what you are doing)
	 */
	@FXML AnchorPane simpleTaskPane;
	@FXML Circle indexCircle;
	@FXML Label indexNumber;
	@FXML Label taskTitle;
	@FXML Label timeText;
	@FXML ImageView doneImage;
	@FXML ImageView pinImage;
	@FXML Separator separator;
	
	// Called after the FXML file specifying this class as fx:controller is loaded
	@FXML
	public void initialize() {		
		assert simpleTaskPane != null : "fx:id=\"simpleTaskPane\" was not injected";
		assert indexCircle != null : "fx:id=\"indexCircle\" was not injected";
		assert indexNumber != null : "fx:id=\"indexNumber\" was not injected";
		assert taskTitle != null : "fx:id=\"taskTitle\" was not injected";
		assert timeText != null : "fx:id=\"timeText\" was not injected";
		assert pinImage != null : "fx:id=\"pinImage\" was not injected";
		assert doneImage != null : "fx:id=\"doneImage\" was not injected";
		assert separator != null : "fx:id=\"separator\" was not injected";
	}
	
	/**
	 * A constructor to create the simple task view for a task.
	 * 
	 * @param task Task to create the simple task view from.
	 * @param index Display index number of the task.
	 * @param separate True if separator is to be drawn.
	 */
	public SimpleTaskController(Task task, int index, boolean separate) {
		super();

		UiUtils.loadFxml(this, this, simpleTaskViewFxmlUrl);
		
		showSeparator(separate);
		setTaskTitle(task.getDescription());
		setIndexNumber(index);
		showPin(task.isPinned());
		showDone(task.isDone());
		
		if (task.getStart() == null && task.getEnd() == null) {
			// floating
			setCircleColor(UiOptions.GREEN);
		} else if (task.getStart() != null && task.getEnd() != null) {
			// start && end
			setCircleColor(UiOptions.BLUE);
		} else if (task.getStart() != null) {
			// start only
			setCircleColor(UiOptions.AMBER);
		} else if (task.getEnd() != null) {
			// end only
			setCircleColor(UiOptions.LIGHT_RED);
		} else {
			LOG.error("Invalid task passed to SimpleTaskController. UUID: " + 
									task.getTaskId().toString());
			throw new Error("Invalid task!");
		}
		
		setTimeText(generateFullTimeText(task.getStart(), task.getEnd(), new DateTime()));
		
		if (isTaskOverdue && !task.isDone()) {
			setTextColor(UiOptions.WARNING_RED);
		}
		
		if (task.isDone()) {
			setTextColor(UiOptions.LIGHT_GREY);
		}
	}
	
	/**
	 * Generates the easy-to-read text displaying the next major time/date
	 * relating to this task. Its main job is to determine whether the task is
	 * starting, has started, is ending, or has ended, and delegates the actual
	 * text generation to other methods.
	 * 
	 * Also sets the isTaskOverdue flag as it does that determining.
	 * @param start Start datetime of the task.
	 * @param end End datetime of the task.
	 * @param current Current datetime.
	 * @return Full text string to display.
	 */
	private String generateFullTimeText(DateTime start, DateTime end,
			DateTime current) {

		if (start == null && end == null) {
			// Task has no times
			return null;
		} 
		
		DateTimeComparator timeComparator = DateTimeComparator.getTimeOnlyInstance();
		
		if (start != null && end != null) {
			// Determine whether task has started or not
			
			int daysBetweenStart = Days.daysBetween(current.toLocalDate(), 
					start.toLocalDate()).getDays();
			int daysBetweenEnd = Days.daysBetween(current.toLocalDate(), 
					end.toLocalDate()).getDays();
			
			/* Task starts at least a day after today */
			// Task has not yet started, so just display START_TEXT
			if (daysBetweenStart > 0) {
				return generateDateTimeText(current, start, Status.START);
			}
			
			/* Task has started at least a day before today */
			// Check when the task ends to determine what to display
			if (daysBetweenStart < 0) {
				/* Task ends at least a day after today */
				if (daysBetweenEnd > 0) {
					return generateDateTimeText(current, end, Status.END);
				} 
				
				/* Task has ended at least a day before today */
				if (daysBetweenEnd < 0) {
					isTaskOverdue = true;
					return generateDateTimeText(current, end, Status.ENDED);
				}
				
				/* Task ends today */
				// Check at what time the task ends to determine what to display
				int endTimeCompare = timeComparator.compare(current, end);
				
				/* Task ends now or later today: current <= end */
				if (endTimeCompare <= 0) {
					return generateDateTimeText(current, end, Status.END);
				}
				
				/* Task has ended earlier today: current > end */
				if (endTimeCompare > 0) {
					isTaskOverdue = true;
					return generateDateTimeText(current, end, Status.ENDED);
				}
			}
			
			/* Task starts today */
			// Need to compare times to determine if task has started or ended
			if (daysBetweenStart == 0) {
				int startTimeCompare = timeComparator.compare(current, start);
				int endTimeCompare = timeComparator.compare(current, end);
				
				/* Task starts now or later today: current <= start */
				if (startTimeCompare <= 0) {
					return generateDateTimeText(current, start, Status.START);
				}
				
				/* Task has started earlier today */
				// Compare end times to see if it has ended
				
				/* Task ends now or later today: current <= end */
				if (endTimeCompare <= 0) {
					return generateDateTimeText(current, end, Status.END);
				}
				
				/* Task has ended earlier today: current > end */
				if (endTimeCompare > 0) {
					isTaskOverdue = true;
					return generateDateTimeText(current, end, Status.ENDED);
				}
			}
			
			LOG.error("Invalid set of compares not caught at assertion");
			throw new Error("Invalid set of compares not caught at assertion");
		}
		
		if (start != null) {
			// Determine if the task has started, then display the start date/time
			int daysBetweenStart = Days.daysBetween(current.toLocalDate(), 
					start.toLocalDate()).getDays();
			
			/* Task starts at least a day after today */
			if (daysBetweenStart > 0) {
				return generateDateTimeText(current, start, Status.START);
			}
			
			/* Task has started at least a day before today */
			if (daysBetweenStart < 0) {
				return generateDateTimeText(current, start, Status.STARTED);
			}
			
			/* Task starts today */
			// Need to check if it starts later today or has started earlier today
			int startTimeCompare = timeComparator.compare(current, start);
			
			/* Task starts now or later today: current <= start */
			if (startTimeCompare <= 0) {
				return generateDateTimeText(current, start, Status.START);
			}
			
			/* Task has started earlier today: current > start */
			if (startTimeCompare > 0) {
				isTaskOverdue = true;
				return generateDateTimeText(current, start, Status.STARTED);
			}
			
			LOG.error("Stray case in start-date-only in generateDateText()!");
			throw new Error("Stray case in start-date-only in generateDateText()!");
		}
		
		if (end != null) {
			// Determine if the task has ended, then display the end date/time
			int daysBetweenEnd = Days.daysBetween(current.toLocalDate(),
					end.toLocalDate()).getDays();
			
			/* Task ends at least a day after today */
			if (daysBetweenEnd > 0) {
				return generateDateTimeText(current, end, Status.END);
			}
			
			/* Task has ended at least a day before today */
			if (daysBetweenEnd < 0) {
				isTaskOverdue = true;
				return generateDateTimeText(current, end, Status.ENDED);
			}
			
			/* Task ends today */
			// Determine if task has ended, then display the end date/time
			int endTimeCompare = timeComparator.compare(current, end);
			
			/* Task ends now or later today: current <= end */
			if (endTimeCompare <= 0) {
				return generateDateTimeText(current, end, Status.END);
			}
			
			/* Task has ended earlier today: current > end */
			if (endTimeCompare > 0) {
				isTaskOverdue = true;
				return generateDateTimeText(current, end, Status.ENDED);
			}
		} 

		LOG.error("Invalid combination of start and end date/times");
		throw new Error("Invalid combination of start and end date/times");
	}
	
	/**
	 * Formats the string to be returned in generateFullTimeText() with the
	 * correct date and time text, which are generated by their respective
	 * methods.
	 * @param current Current datetime.
	 * @param other Other datetime (start or end).
	 * @param status Status of the task as defined in the enum Status.
	 * @return Formatted full string ready to be displayed in the task view.
	 */
	private String generateDateTimeText(DateTime current, DateTime other, 
			Status status) {
		switch (status) {
			case START:
				return String.format(START_TEXT, generateDateText(current, other), 
						generateTimeText(other));
			case STARTED:
				return String.format(STARTED_TEXT, generateDateText(current, other), 
						generateTimeText(other));
			case END:
				return String.format(END_TEXT, generateDateText(current, other), 
						generateTimeText(other));
			case ENDED:
				return String.format(ENDED_TEXT, generateDateText(current, other), 
						generateTimeText(other));
			default:
				LOG.error("Invalid status at generateDateTimeText()!");
				throw new Error("Invalid status at generateDateTimeText()!");	
		}
	}
	
	/**
	 * Uses a DateTimeFormatter to format the time to be displayed.
	 * @param time Time to be displayed.
	 * @return Formatted string of the time to be displayed.
	 */
	private String generateTimeText(DateTime time) {
		return time.toLocalTime().toString(DateTimeFormatters.time24Formatter);
	}

	/**
	 * Formats the date to be displayed by determining if a more natural 
	 * date text can be displayed (e.g. "tomorrow" instead of long-form date)
	 * and formats the date if it is long-form.
	 * @param current Current datetime
	 * @param other Other datetime (start or end).
	 * @return Formatted string of the date to be displayed.
	 */
	private String generateDateText(DateTime current, DateTime other) {
		int daysBetweenDates = Days.daysBetween(current.toLocalDate(), 
				other.toLocalDate()).getDays();
		
		if (daysBetweenDates == 0) {
			// both dates are on the same day
			return TODAY;
		}
	
		if (daysBetweenDates == -1) {
			return YESTERDAY;
		}
		
		if (daysBetweenDates == 1) {
			return TOMORROW;
		}
		
		// Check weeks
		int weeksBetweenDates = Weeks.weeksBetween(current.toLocalDate(),
				other.toLocalDate()).getWeeks();
		
		if (weeksBetweenDates == 0) {
			// Check if it is earlier this week or later this week
			if (daysBetweenDates < 0) {
				return THISWEEK_EARLIER + generateDayOfWeekString(other.getDayOfWeek());
			}
			if (daysBetweenDates > 0) {
				return THISWEEK_LATER + generateDayOfWeekString(other.getDayOfWeek());
			}
			
			throw new Error("daysBetweenDates == 0 during week comparison");
		}
		
		return String.format(SOME_DATE, 
				other.toLocalDate().toString(DateTimeFormatters.stdDateFormatter));
	}
	
	/**
	 * Generates the corresponding day of the week in a String,
	 * based on the mapping defined in joda's DateTimeConstants
	 * @param jodaDayOfWeek
	 * @return Day of the week as a string
	 */
	private String generateDayOfWeekString(int jodaDayOfWeek) {
		assert jodaDayOfWeek >= 1 && jodaDayOfWeek <= 7 : "Invalid day of week!";
		
		switch (jodaDayOfWeek) {
			case 1:
				return "Monday";
			case 2:
				return "Tuesday";
			case 3:
				return "Wednesday";
			case 4:
				return "Thursday";
			case 5:
				return "Friday";
			case 6:
				return "Saturday";
			case 7:
				return "Sunday";
			default:
				throw new Error("Invalid int in generateDayOfWeekString() not caught!");
		}
	}
	
	/**
	 * Sets the color of all text elements of the task view.
	 * @param color String representing the color to set the text.
	 */
	private void setTextColor(String color) {
		taskTitle.setTextFill(Paint.valueOf(color));
		timeText.setTextFill(Paint.valueOf(color));
	}
	
	/* Private setters for setting the task's attributes */
	
	private void setTaskTitle(String title) {
		taskTitle.setText(title);
	}
	
	private void setTimeText(String time) {
		timeText.setText(time);
	}
	
	private void setIndexNumber(int index) {
		indexNumber.setText(Integer.toString(index));
	}
	
	private void setCircleColor(String color) {
		indexCircle.setFill(Paint.valueOf(color));
	}
	
	private void showPin(boolean show) {
		pinImage.setVisible(show);
	}
	
	private void showDone(boolean show) {
		doneImage.setVisible(show);
	}
	
	private void showSeparator(boolean show) {
		separator.setVisible(show);
	}
}
